package db

import (
	"context"
	"errors"
	"github.com/mitchellh/mapstructure"
	"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)

const (
	uuid_key              = `elementId`
	node_labels_key       = `labels`
	relationship_type_key = `type`
	relationship_from_key = `startElementId`
	relationship_end_key  = `endElementId`
)

type Paged[T any] struct {
	Total uint `mapstructure:"total" json:"total"`
	Rows  []*T
}

type _BaseModel_ struct {
	Uuid string `mapstructure:"elementId" json:"uuid"`
}

type _Node_ struct {
	_BaseModel_ `mapstructure:",squash"`
	Labels      []string `mapstructure:"labels" json:"labels"`
}

type _Relationship_ struct {
	_BaseModel_ `mapstructure:",squash"`
	Type        string `mapstructure:"type" json:"type"`
	From        string `mapstructure:"startElementId" json:"from"`
	To          string `mapstructure:"endElementId" json:"to"`
}

type NodeModel interface {
	User | Role
}

type NodeLoader[T NodeModel] struct {
	model *T
}

func NewNodeLoader[T NodeModel](model *T) *NodeLoader[T] {
	return &NodeLoader[T]{
		model,
	}
}

func (nl *NodeLoader[T]) LoadT(ctx context.Context, result neo4j.ResultWithContext, key string) error {
	node, err := neo4j.SingleTWithContext[neo4j.Node](ctx, result,
		func(record *neo4j.Record) (neo4j.Node, error) {
			n, _, err := neo4j.GetRecordValue[neo4j.Node](record, key)
			return n, err
		})
	if err != nil {
		return err
	}
	maps := node.GetProperties()
	maps[uuid_key] = node.ElementId
	maps[node_labels_key] = node.Labels

	mapstructure.Decode(maps, nl.model)
	return nil
}

func (nl *NodeLoader[T]) LoadE(ctx context.Context, result *neo4j.EagerResult, key string) error {
	if len(result.Records) == 0 {
		return errors.New("not found entity")
	}
	node, _, err := neo4j.GetRecordValue[neo4j.Node](result.Records[0], key)
	if err != nil {
		return err
	}
	maps := node.GetProperties()
	maps[uuid_key] = node.ElementId
	maps[node_labels_key] = node.Labels

	mapstructure.Decode(maps, nl.model)
	return nil
}

type RelationshipModel interface {
	UserBelongsToRole
}

type RelationshipLoader[T RelationshipModel] struct {
	rel *T
}

func NewRelationshipLoader[T RelationshipModel](rel *T) *RelationshipLoader[T] {
	return &RelationshipLoader[T]{
		rel,
	}
}

func (rl *RelationshipLoader[T]) LoadT(ctx context.Context, result neo4j.ResultWithContext, key string) error {
	rel, err := neo4j.SingleTWithContext[neo4j.Relationship](ctx, result,
		func(record *neo4j.Record) (neo4j.Relationship, error) {
			rel, _, err := neo4j.GetRecordValue[neo4j.Relationship](record, key)
			return rel, err
		})
	if err != nil {
		return err
	}
	maps := rel.GetProperties()
	maps[uuid_key] = rel.ElementId
	maps[relationship_type_key] = rel.Type
	maps[relationship_from_key] = rel.StartElementId
	maps[relationship_end_key] = rel.EndElementId

	mapstructure.Decode(maps, rl.rel)
	return nil
}

func (rl *RelationshipLoader[T]) LoadE(ctx context.Context, result *neo4j.EagerResult, key string) error {
	if len(result.Records) == 0 {
		return errors.New("not found relationship")
	}
	rel, _, err := neo4j.GetRecordValue[neo4j.Relationship](result.Records[0], key)
	if err != nil {
		return err
	}
	maps := rel.GetProperties()
	maps[uuid_key] = rel.ElementId
	maps[relationship_type_key] = rel.Type
	maps[relationship_from_key] = rel.StartElementId
	maps[relationship_end_key] = rel.EndElementId

	mapstructure.Decode(maps, rl.rel)
	return nil
}

type QueryModel interface {
	UserWithRole
}

type QueryLoader[T QueryModel] struct {
	rows []*T
}

func NewQueryLoader[T QueryModel](len uint) *QueryLoader[T] {
	return &QueryLoader[T]{
		rows: make([]*T, 0, len),
	}
}

func (ql *QueryLoader[T]) LoadE(ctx context.Context, result *neo4j.EagerResult) error {
	for i, record := range result.Records {
		ql.rows = append(ql.rows, new(T))
		mapstructure.Decode(record.AsMap(), ql.rows[i])
	}
	return nil
}

func (ql *QueryLoader[T]) Output() []*T {
	return ql.rows
}

type PagedQueryLoader[T QueryModel] struct {
	pq *Paged[T]
}

func NewPagedQueryLoader[T QueryModel](pq *Paged[T]) *PagedQueryLoader[T] {
	return &PagedQueryLoader[T]{
		pq,
	}
}

func (pql *PagedQueryLoader[T]) LoadE(ctx context.Context, result *neo4j.EagerResult) error {
	if len(result.Records) == 0 {
		return errors.New("not found paged data")
	}
	mapstructure.Decode(result.Records[0].AsMap(), pql.pq)
	return nil
}
